{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "source": [
        "> **Note** You can enable type checking in Colab in the menu `Tools` > `Setting` > `Editor` > (scroll to the bottom) Code diagnostics and choose `Syntax and type checking`. It then underlines type errors in red and hovering them displays the message:"
      ],
      "metadata": {
        "id": "e_uvksOvM-_8"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "### **Understanding Generics in Python**\n",
        "Generics allow us to create flexible and reusable code that can handle multiple data types dynamically. They are useful in situations where a function, class, or method should work with **different types** while maintaining **type safety**.\n",
        "\n",
        "In Python, **generics** are implemented using the `typing` module.\n",
        "\n",
        "---\n",
        "\n",
        "## **1. Why Use Generics?**\n",
        "Let's consider an example without generics:\n",
        "\n",
        "```python\n",
        "def double_number(n: int) -> int:\n",
        "    return n * 2\n",
        "\n",
        "print(double_number(5))   # ✅ Works fine\n",
        "print(double_number(5.5)) # ❌ Type error (expected int)\n",
        "```\n"
      ],
      "metadata": {
        "id": "ZdQgkz06LCl6"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "def double_number(n: int) -> int:\n",
        "    return n * 2\n",
        "\n",
        "print(double_number(5))   # ✅ Works fine\n",
        "print(double_number(5.5)) # ❌ Type error (expected int)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "EzoEwyZfLgwO",
        "outputId": "c1a3d159-834c-4414-da48-0630125c4e60"
      },
      "execution_count": 4,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "10\n",
            "11.0\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "This function only works with integers. But what if we want it to work with `float` as well?\n",
        "\n",
        "We could remove the type hints, but then we'd lose **type safety**. Instead, **generics solve this issue!**\n",
        "\n",
        "---\n",
        "\n",
        "## **2. Basic Syntax of Generics (`TypeVar`)**\n",
        "Generics in Python are implemented using `TypeVar` from `typing`.\n",
        "\n",
        "```python\n",
        "from typing import TypeVar\n",
        "\n",
        "T = TypeVar(\"T\")  # T represents a generic type\n",
        "```\n",
        "\n",
        "- `T` is a placeholder that can be **replaced with any type** when the function is called.\n",
        "- The **actual type is inferred at runtime**.\n",
        "\n",
        "---\n",
        "\n",
        "## **3. Using Generics in Functions**\n",
        "```python\n",
        "from typing import TypeVar\n",
        "\n",
        "T = TypeVar(\"T\")  # Generic Type\n",
        "\n",
        "def identity(value: T) -> T:\n",
        "    return value\n",
        "\n",
        "print(identity(5))        # ✅ Works with int\n",
        "print(identity(\"Hello\"))  # ✅ Works with str\n",
        "print(identity([1, 2, 3])) # ✅ Works with list\n",
        "```\n",
        "### **How It Works:**\n",
        "- `T` is a **type variable**, meaning that whatever type we pass, the function adapts.\n",
        "- `identity(5)` → `T` becomes `int`\n",
        "- `identity(\"Hello\")` → `T` becomes `str`\n",
        "- `identity([1, 2, 3])` → `T` becomes `list[int]`\n",
        "\n",
        "✅ **Advantage:** Type checking remains **strong**, but the function is **flexible**!\n",
        "\n",
        "---\n"
      ],
      "metadata": {
        "id": "gaD8--GeLhb2"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import TypeVar\n",
        "\n",
        "T = TypeVar(\"T\")  # Generic Type\n",
        "\n",
        "def identity(value: T) -> T:\n",
        "    return value\n",
        "\n",
        "print(identity(5))        # ✅ Works with int\n",
        "print(identity(\"Hello\"))  # ✅ Works with str\n",
        "print(identity([1, 2, 3])) # ✅ Works with list"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "NuuZgL8oNjz_",
        "outputId": "36bab821-9d62-4792-80be-f44a7a1bb865"
      },
      "execution_count": 6,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "5\n",
            "Hello\n",
            "[1, 2, 3]\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "## **4. Using Generics in Classes**\n",
        "We can define **generic classes** that work with multiple data types.\n",
        "\n",
        "### **Example: Generic Container Class**\n",
        "```python\n",
        "from typing import Generic, TypeVar\n",
        "\n",
        "T = TypeVar(\"T\")  # Define a type variable\n",
        "\n",
        "class Container(Generic[T]):  # Declare a generic class\n",
        "    def __init__(self, value: T):\n",
        "        self.value = value\n",
        "    \n",
        "    def get_value(self) -> T:\n",
        "        return self.value\n",
        "\n",
        "# Creating instances with different types\n",
        "c1 = Container(10)          # T becomes int\n",
        "c2 = Container(\"Python\")    # T becomes str\n",
        "c3 = Container([1, 2, 3])   # T becomes list[int]\n",
        "\n",
        "print(c1.get_value())  # 10\n",
        "print(c2.get_value())  # Python\n",
        "print(c3.get_value())  # [1, 2, 3]\n",
        "```\n",
        "✅ **Advantage:**  \n",
        "- We **don’t need separate classes** for `int`, `str`, `list`, etc.\n",
        "- The class **adapts** to different types while maintaining **type safety**.\n",
        "\n",
        "---\n"
      ],
      "metadata": {
        "id": "OQmc30VNNkge"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import Generic, TypeVar\n",
        "\n",
        "T = TypeVar(\"T\")  # Define a type variable\n",
        "\n",
        "class Container(Generic[T]):  # Declare a generic class\n",
        "    def __init__(self, value: T):\n",
        "        self.value = value\n",
        "\n",
        "    def get_value(self) -> T:\n",
        "        return self.value\n",
        "\n",
        "# Creating instances with different types\n",
        "c1 = Container(10)          # T becomes int\n",
        "c2 = Container(\"Python\")    # T becomes str\n",
        "c3 = Container([1, 2, 3])   # T becomes list[int]\n",
        "\n",
        "print(c1.get_value())  # 10\n",
        "print(c2.get_value())  # Python\n",
        "print(c3.get_value())  # [1, 2, 3]"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "N-h3ZC0VNo37",
        "outputId": "19887201-90be-4869-f798-b4fcb690a43c"
      },
      "execution_count": 7,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "10\n",
            "Python\n",
            "[1, 2, 3]\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "## **5. Generics with Multiple Type Variables**\n",
        "Sometimes, we need more than one **generic type**. We can define **multiple `TypeVar`s**.\n",
        "\n",
        "### **Example: Generic Key-Value Pair**\n",
        "```python\n",
        "from typing import TypeVar, Generic\n",
        "\n",
        "K = TypeVar(\"K\")  # Key type\n",
        "V = TypeVar(\"V\")  # Value type\n",
        "\n",
        "class KeyValuePair(Generic[K, V]):\n",
        "    def __init__(self, key: K, value: V):\n",
        "        self.key = key\n",
        "        self.value = value\n",
        "\n",
        "    def get_pair(self) -> tuple[K, V]:\n",
        "        return (self.key, self.value)\n",
        "\n",
        "pair1 = KeyValuePair(\"id\", 101)       # str, int\n",
        "pair2 = KeyValuePair(1, \"Python\")     # int, str\n",
        "\n",
        "print(pair1.get_pair())  # ('id', 101)\n",
        "print(pair2.get_pair())  # (1, 'Python')\n",
        "```\n",
        "✅ **Advantage:**  \n",
        "- Works with **any combination of key-value types**.\n",
        "\n",
        "---\n"
      ],
      "metadata": {
        "id": "7phKDP3_NpFk"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import TypeVar, Generic\n",
        "\n",
        "K = TypeVar(\"K\")  # Key type\n",
        "V = TypeVar(\"V\")  # Value type\n",
        "\n",
        "class KeyValuePair(Generic[K, V]):\n",
        "    def __init__(self, key: K, value: V):\n",
        "        self.key = key\n",
        "        self.value = value\n",
        "\n",
        "    def get_pair(self) -> tuple[K, V]:\n",
        "        return (self.key, self.value)\n",
        "\n",
        "pair1 = KeyValuePair(\"id\", 101)       # str, int\n",
        "pair2 = KeyValuePair(1, \"Python\")     # int, str\n",
        "\n",
        "print(pair1.get_pair())  # ('id', 101)\n",
        "print(pair2.get_pair())  # (1, 'Python')"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "EvcjfkUxNyPu",
        "outputId": "b5ba7067-73de-4b56-dbf2-14098c98306e"
      },
      "execution_count": 8,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "('id', 101)\n",
            "(1, 'Python')\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "## **6. Generics with Constraints (`bound=`)**\n",
        "We can **restrict** the generic type to **a specific superclass**.\n",
        "\n",
        "### **Example: Restricting Generics to Numeric Types**\n",
        "```python\n",
        "from typing import TypeVar\n",
        "\n",
        "# TypeVar bound to (restricted to) float and int types\n",
        "Number = TypeVar(\"Number\", int, float)\n",
        "\n",
        "def add(x: Number, y: Number) -> Number:\n",
        "    return x + y\n",
        "\n",
        "print(add(3, 5))     # ✅ Works with int\n",
        "print(add(2.5, 1.2)) # ✅ Works with float\n",
        "print(add(\"3\", \"5\")) # ❌ Type error: str is not allowed\n",
        "```\n",
        "✅ **Advantage:**  \n",
        "- Ensures **only numbers** are accepted (not strings, lists, etc.).\n",
        "\n",
        "---\n"
      ],
      "metadata": {
        "id": "CCZ38Ml-NyrZ"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import TypeVar\n",
        "\n",
        "# TypeVar bound to (restricted to) float and int types\n",
        "Number = TypeVar(\"Number\", int, float)\n",
        "\n",
        "def add(x: Number, y: Number) -> Number:\n",
        "    return x + y\n",
        "\n",
        "print(add(3, 5))     # ✅ Works with int\n",
        "print(add(2.5, 1.2)) # ✅ Works with float\n",
        "print(add(\"3\", \"5\")) # ❌ Type error: str is not allowed"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "-D3CTyK7N1g6",
        "outputId": "9c380e8f-21cd-4dea-f10c-51cbeaee0e95"
      },
      "execution_count": 9,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "8\n",
            "3.7\n",
            "35\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "## **7. Generics with Data Structures (`list[T]`, `dict[K, V]`)**\n",
        "Generics are often used with **data structures**.\n",
        "\n",
        "### **Example: Generic Stack Implementation**\n",
        "```python\n",
        "from typing import TypeVar, Generic\n",
        "\n",
        "T = TypeVar(\"T\")\n",
        "\n",
        "class Stack(Generic[T]):\n",
        "    def __init__(self):\n",
        "        self.items: list[T] = []\n",
        "\n",
        "    def push(self, item: T) -> None:\n",
        "        self.items.append(item)\n",
        "\n",
        "    def pop(self) -> T:\n",
        "        return self.items.pop()\n",
        "\n",
        "    def is_empty(self) -> bool:\n",
        "        return len(self.items) == 0\n",
        "\n",
        "stack_int = Stack[int]()\n",
        "stack_int.push(10)\n",
        "stack_int.push(20)\n",
        "\n",
        "print(stack_int.pop())  # 20\n",
        "print(stack_int.pop())  # 10\n",
        "```\n",
        "✅ **Advantage:**  \n",
        "- A `Stack[int]` ensures that only integers are stored.\n",
        "- If you try `stack_int.push(\"hello\")`, **Python will raise a type error**.\n",
        "\n",
        "---\n",
        "\n"
      ],
      "metadata": {
        "id": "UmujDY3cN1_v"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import TypeVar, Generic\n",
        "\n",
        "T = TypeVar(\"T\")\n",
        "\n",
        "class Stack(Generic[T]):\n",
        "    def __init__(self):\n",
        "        self.items: list[T] = []\n",
        "\n",
        "    def push(self, item: T) -> None:\n",
        "        self.items.append(item)\n",
        "\n",
        "    def pop(self) -> T:\n",
        "        return self.items.pop()\n",
        "\n",
        "    def is_empty(self) -> bool:\n",
        "        return len(self.items) == 0\n",
        "\n",
        "stack_int = Stack[int]()\n",
        "stack_int.push(10)\n",
        "stack_int.push(20)\n",
        "\n",
        "print(stack_int.pop())  # 20\n",
        "print(stack_int.pop())  # 10"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "amWUDohiN5Fi",
        "outputId": "9d26030d-7753-41e5-df1a-b596ba36a979"
      },
      "execution_count": 10,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "20\n",
            "10\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **8. Generics in Function Parameters (`Callable`)**\n",
        "Some functions accept **another function** as a parameter. We can use `Callable` with generics.\n",
        "\n",
        "### **Example: Generic Function as Parameter**\n",
        "```python\n",
        "from typing import TypeVar, Callable\n",
        "\n",
        "T = TypeVar(\"T\")\n",
        "\n",
        "def apply_function(func: Callable[[T], T], value: T) -> T:\n",
        "    return func(value)\n",
        "\n",
        "def square(n: int) -> int:\n",
        "    return n * n\n",
        "\n",
        "print(apply_function(square, 5))  # 25\n",
        "```\n",
        "✅ **Advantage:**  \n",
        "- The function **adapts dynamically** to different functions passed as arguments.\n",
        "\n",
        "---\n",
        "\n"
      ],
      "metadata": {
        "id": "eSp-YhP4N5ib"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import TypeVar, Callable\n",
        "\n",
        "T = TypeVar(\"T\")\n",
        "\n",
        "def apply_function(func: Callable[[T], T], value: T) -> T:\n",
        "    return func(value)\n",
        "\n",
        "def square(n: int) -> int:\n",
        "    return n * n\n",
        "\n",
        "print(apply_function(square, 5))  # 25"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "YNo90NkuOlg2",
        "outputId": "3b702f1c-59df-4593-a4e6-3b542dde31a3"
      },
      "execution_count": 11,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "25\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **9. Advanced: Using `Generic[T]` in LLM-based Agents**\n",
        "Here’s a **real-world example** where `Generic[T]` is used in an **AI agent** class.\n",
        "\n",
        "```python\n",
        "from typing import Generic, TypeVar\n",
        "\n",
        "TContext = TypeVar(\"TContext\")\n",
        "\n",
        "class Agent(Generic[TContext]):\n",
        "    \"\"\"A generic AI agent that works with different contexts.\"\"\"\n",
        "\n",
        "    def __init__(self, name: str, context: TContext):\n",
        "        self.name = name\n",
        "        self.context = context\n",
        "\n",
        "    def execute(self) -> None:\n",
        "        print(f\"Executing with context: {self.context}\")\n",
        "\n",
        "# Creating agents with different contexts\n",
        "text_agent = Agent[str](\"TextProcessor\", \"Analyze sentiment\")\n",
        "data_agent = Agent[dict](\"DataAnalyzer\", {\"data\": [1, 2, 3]})\n",
        "\n",
        "text_agent.execute()  # Executing with context: Analyze sentiment\n",
        "data_agent.execute()  # Executing with context: {'data': [1, 2, 3]}\n",
        "```\n",
        "✅ **Why is this useful?**  \n",
        "- The **same `Agent` class** works with **any context type** (text, dict, etc.).\n",
        "- It **avoids code duplication**.\n",
        "\n",
        "---\n"
      ],
      "metadata": {
        "id": "Rh49VeapOo85"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from typing import Generic, TypeVar\n",
        "\n",
        "TContext = TypeVar(\"TContext\")\n",
        "\n",
        "class Agent(Generic[TContext]):\n",
        "    \"\"\"A generic AI agent that works with different contexts.\"\"\"\n",
        "\n",
        "    def __init__(self, name: str, context: TContext):\n",
        "        self.name = name\n",
        "        self.context = context\n",
        "\n",
        "    def execute(self) -> None:\n",
        "        print(f\"Executing with context: {self.context}\")\n",
        "\n",
        "# Creating agents with different contexts\n",
        "text_agent = Agent[str](\"TextProcessor\", \"Analyze sentiment\")\n",
        "data_agent = Agent[dict](\"DataAnalyzer\", {\"data\": [1, 2, 3]})\n",
        "\n",
        "text_agent.execute()  # Executing with context: Analyze sentiment\n",
        "data_agent.execute()  # Executing with context: {'data': [1, 2, 3]}"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "CdEuXUbrN8YW",
        "outputId": "6fc360a3-ed48-41a2-c517-602df66bd3c0"
      },
      "execution_count": 12,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Executing with context: Analyze sentiment\n",
            "Executing with context: {'data': [1, 2, 3]}\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "## **Final Summary**\n",
        "| Concept | Explanation |\n",
        "|---------|------------|\n",
        "| **TypeVar** | Defines a generic type variable (`T`) |\n",
        "| **Generic[T]** | Creates classes that work with different types |\n",
        "| **field(default_factory=list)** | Ensures safe initialization of mutable types |\n",
        "| **Callable** | Defines function signatures with generics |\n",
        "| **Bounded TypeVar** | Restricts generics to specific types (e.g., `int`, `float`) |\n",
        "| **Multiple TypeVars** | Supports multiple generic types (`K, V`) |\n",
        "\n",
        "---\n",
        "\n",
        "### **🚀 Key Takeaways**\n",
        "- Generics **reduce code duplication** and **increase flexibility**.\n",
        "- They **work with functions, classes, and complex structures**.\n",
        "- `TypeVar` allows **defining flexible data types**.\n",
        "- Use `Generic[T]` to **create reusable AI models, agents, or data structures**.\n",
        "\n",
        "Would you like to practice some examples? 🚀"
      ],
      "metadata": {
        "id": "Zdr9iHo-N_4Z"
      }
    }
  ]
}